﻿using OpenBlock.Block;
using OpenBlock.IO;
using OpenBlock.Terrain.Generators;
using OpenBlock.Utils;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;

namespace OpenBlock.Terrain
{
    public class Level
    {
        public const int CHUNK_LOAD_XOZ_RADIUS = 4;
        public const int CHUNK_LOAD_HEIGHT_RADIUS = 2;

        private readonly static Vector3Int[] NEIBOUR_CHUNK_POS = new Vector3Int[]
        {
            Vector3Int.up, Vector3Int.right, Vector3Int.forward, Vector3Int.down, Vector3Int.left,  Vector3Int.back
        };

        public string Name { get; private set; }

        // Pool
        private List<Chunk> chunks;
        private int nextLoadIdx;

        private List<Vector3Int> savedChunks;
        private string chunksPath;

        private ITerrainGenerator generator;

        private bool isTempLevel;

        public Level(ITerrainGenerator generator)
        {
            this.generator = generator;

            Name = "Temp";
            chunks = new List<Chunk>();
            nextLoadIdx = 0;
            savedChunks = null;

            isTempLevel = true;
        }

        public Level(string name, int seed, int type)
        {
            if (type == 0)
            {
                generator = new SkyIslandGenerator(seed);
            }

            Name = name;

            chunks = new List<Chunk>();
            nextLoadIdx = 0;

            savedChunks = new List<Vector3Int>();
            chunksPath = Path.Combine(LevelManager.LevelDirectoryPath, Name, "chunks");
            foreach (var chunk in Directory.GetFiles(chunksPath, "m.*.obc"))
            {
                var position = Path.GetFileName(chunk).Split('.');
                savedChunks.Add(new Vector3Int(int.Parse(position[1]), int.Parse(position[2]), int.Parse(position[3])));
            }

            isTempLevel = false;
        }

        public BlockState GetBlockState(Vector3Int blockPos)
        {
            if (GetChunk(MathUtils.BlockPos2ChunkPos(blockPos)) is Chunk chunk)
            {
                return chunk.GetBlock(MathUtils.BlockPos2InternalPos(blockPos));
            }
            return BlockState.AIR;
        }

        public void DestroyBlock(Vector3Int blockPos)
        {
            if (GetChunk(MathUtils.BlockPos2ChunkPos(blockPos)) is Chunk chunk)
            {
                chunk.DestroyBlock(MathUtils.BlockPos2InternalPos(blockPos));
            }
        }

        public void SetBlock(BlockState state, Vector3Int blockPos)
        {
            if (GetChunk(MathUtils.BlockPos2ChunkPos(blockPos)) is Chunk chunk)
            {
                chunk.SetBlock(state, MathUtils.BlockPos2InternalPos(blockPos));
            }
        }

        public Chunk GetChunk(Vector3Int chunkPos)
        {
            foreach (var chunk in EnumerateLoadedChunks())
            {
                if (chunk.chunkPos == chunkPos)
                {
                    return chunk;
                }
            }
            return null;
        }

        private Chunk LoadChunk(Vector3Int chunkPos)
        {
            // Step 1: Retrieve or create a Chunk
            Chunk ch;
            if (nextLoadIdx < chunks.Count)
            {
                ch = chunks[nextLoadIdx];
            }
            else
            {
                ch = new Chunk();
                chunks.Add(ch);
            }
            nextLoadIdx += 1;

            // Step 2: Fill this chunk
            if (isTempLevel || !savedChunks.Contains(chunkPos))
            {
                ch.Reset(chunkPos);
                generator.GenerateTerrain(ref ch);
            }
            else
            {
                using var chunkFile = File.OpenRead(Path.Combine(chunksPath, GetChunkFileName(chunkPos)));
                using var reader = new BinaryReader(chunkFile);
                ch.LoadFromReader(reader);
            }

            // Step 3: Find it relationships
            #region Link New Chunk
            foreach (var chunk in EnumerateLoadedChunks())
            {
                for (int j = 0; j < NEIBOUR_CHUNK_POS.Length; ++j)
                {
                    if (chunk.chunkPos == chunkPos + NEIBOUR_CHUNK_POS[j])
                    {
                        ch.neighbours[j] = chunk;
                        chunk.neighbours[(j + 3) % 6] = ch;
                        chunk.status |= Chunk.ChunkStatus.NeedRebuild;
                    }
                }
            }
            #endregion

            // Step 4: Return it to level.
            return ch;
        }

        public void LoadChunks(Vector3Int center)
        {
            int xStart = center.x - CHUNK_LOAD_XOZ_RADIUS;
            int xEnd = center.x + CHUNK_LOAD_XOZ_RADIUS;
            int yStart = center.y - CHUNK_LOAD_HEIGHT_RADIUS;
            int yEnd = center.y + CHUNK_LOAD_HEIGHT_RADIUS;
            int zStart = center.z - CHUNK_LOAD_XOZ_RADIUS;
            int zEnd = center.z + CHUNK_LOAD_XOZ_RADIUS;

            var inRangeChunkIndices = new List<int>();

            for (int x = xStart; x < xEnd; x++)
            {
                for (int y = yStart; y < yEnd; y++)
                {
                    for (int z = zStart; z < zEnd; z++)
                    {
                        var pos = new Vector3Int(x, y, z);
                        var hadLoaded = false;
                        for (int i = 0; i < nextLoadIdx; i++)
                        {
                            if (pos == chunks[i].chunkPos)
                            {
                                inRangeChunkIndices.Add(i);
                                hadLoaded = true;
                                break;
                            }
                        }

                        if (!hadLoaded)
                        {
                            LoadChunk(pos);
                        }
                    }
                }
            }

            for (int i = 0; i < nextLoadIdx; i++)
            {
                if (!inRangeChunkIndices.Contains(i))
                {
                    UnloadChunk(i);
                }
            }
        }

        public void UnloadChunk(int idx)
        {
            if (idx < 0 || idx >= nextLoadIdx) return;

            // Unlink chunks
            for (int j = 0; j < NEIBOUR_CHUNK_POS.Length; j++)
            {
                if (chunks[idx].neighbours[j] != null)
                {
                    chunks[idx].neighbours[j].neighbours[(j + 3) % 6] = null;
                    chunks[idx].neighbours[j] = null;
                }
            }

            chunks[idx].status |= Chunk.ChunkStatus.Inactive;

            if (!isTempLevel)
            {
                using var writer = new BinaryWriter(File.OpenWrite(Path.Combine(chunksPath, GetChunkFileName(chunks[idx].chunkPos))));
                chunks[idx].Save(writer);
                if (!savedChunks.Contains(chunks[idx].chunkPos)) savedChunks.Add(chunks[idx].chunkPos);
            }

            if (idx != nextLoadIdx - 1)
            {
                var t = chunks[nextLoadIdx - 1];
                chunks[nextLoadIdx - 1] = chunks[idx];
                chunks[idx] = t;
            }

            nextLoadIdx -= 1;
        }

        public void UnloadChunkAt(Vector3Int chunkPos)
        {
            for (int i = 0; i < nextLoadIdx; i++)
            {
                if (chunks[i].chunkPos == chunkPos)
                {
                    UnloadChunk(i);
                    break;
                }
            }
        }

        public void UnloadAllChunk()
        {
            for (int i = nextLoadIdx - 1; i >= 0; i--)
            {
                UnloadChunk(i);
            }
        }

        public IEnumerable<Chunk> EnumerateLoadedChunks()
        {
            for (int i = 0; i < nextLoadIdx; i++) yield return chunks[i];
        }

        private static string GetChunkFileName(Vector3Int chunkPos)
        {
            return $"m.{chunkPos.x}.{chunkPos.y}.{chunkPos.z}.obc";
        }
    }
}
